-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow handlers to return user-defined error types #1180
base: main
Are you sure you want to change the base?
Conversation
Enforce that `HttpResponseError`s status codes are 4xx or 5xx using an `ErrorStatusCode` type which may only be those statuses. See: #39 (comment) While we're making breaking API changes, let's also have `HttpError::for_status` take a validated client-error-only type, rather than panicking surprisingly. This way, it's obvious to the user that the argument to this has to be a 4xx. Fixes #693
it occurs to me that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for doing this
dropshot/src/api_description.rs
Outdated
@@ -919,27 +925,59 @@ impl<Context: ServerContext> ApiDescription<Context> { | |||
} | |||
}; | |||
|
|||
// If the endpoint defines an error type, emit that for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, we could have added to components.responses
as before and then referenced that. I can see the inline approach you've taken as potentially simpler, though it does bloat up the json output...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I'd like to put them in components.responses
, too. The reason I didn't is that it might be a bit annoying to determine the name for each response schema. schemars
internally disambiguates colliding schema names by turning subsequent ones into like Error2
or whatever, but (AFAICT) we only get that when we actually generate the schema and it gives us back a reference (into components.schemas
). We could then try to parse that reference and get the name back out to then use it to generate a components.responses
entry for that response, which seems possible, I just thought it seemed annoying enough that I didn't really want to bother with it. Do you think it's worth doing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we name the response based on <T as JsonSchema>::schema_name()
? Might that work?
Do I think it's worth doing? I think it's worth trying. It might make the code worse, but it might make the output simpler. At a minimum it will make the diffs against current json simpler. These together--I think--at least warrant giving it a shot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe that deduplication is applied to JsonSchema::schema_name()
; as
far as I can tell, it only happens once a schema has already been generated,
because that's when the generator can check if the name already exists in the
set of schemas that have been generated so far?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah. Unfortunately, it turns out that Progenitor chokes on outputting two separate response objects that have the same response body schema; it currently expects there to be exactly one response object for errors, as the current code hits this assertion.
I'm on the fence about whether the right solution here is to change the OpenAPI spec generation in Dropshot to emit a single response schema object for 4xx and 5xx errors, or to make Progenitor smart enough to understand that multiple response schemas can share the same response body type schema, and recognize that those are the same thing. What do you think @ahl?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the OpenAPI input it's choking on? I wouldn't have thought that this move away from components/responses
would have had this result.
With regard to progenitor supporting it; I would like to do that, but I would also love to be able to defer that. It's not a small amount of work to do reasonably well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Progenitor panics when building oxidecomputer/omicron#7196, which updates Omicron to depend on this branch. Note that the only change to the schemas is to generate separate response schemas for 4xx and 5xxs, as that PR hasn't added any user-defined error types or anything. It'll die on any of the OpenAPI docs in that PR, which one it hits first seems to be non-deterministic and depends on the order in which crates are built. For an example of the OpenAPI change, see https://github.com/oxidecomputer/omicron/pull/7196/files#diff-a389634108eedc06638f772e1384ef397e4d4254363b3459e5043341227f06b6.
I'm going to take another crack at having Dropshot generate one response schema for both error status codes, which would hopefully resolve the issue without requiring a change to Progenitor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm genuinely surprised that progenitor is dying despite its constraints. My recollection was that a reference to a response would be handled identically to an inline response. I'm going to look on the progenitor side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay @ahl, if you look at the PR comparison view again as of commit f0e6a46, you'll see that there's now no diff in any of the generated OpenAPI documents compared to main
; the only diff in the expectorate output now is the new schemas with user-defined error types!
So, while it would be nice for progenitor to be able to handle cases like this, APIs that don't use custom error types will now emit backwards-compatible OpenAPI documents that the current version of Progenitor ought to be able to handle correctly.
@@ -943,9 +946,9 @@ async fn http_request_handle<C: ServerContext>( | |||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we be firing a USDT probe here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We weren't previously, but yeah, we probably should. I can add it in this PR if that makes sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably doesn't... (unless you've done it already). We can file an issue
Co-authored-by: Adam Leventhal <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice work
"content": { | ||
"application/json": { | ||
"schema": { | ||
"$ref": "#/components/schemas/EnumError" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
neat!
Co-authored-by: Adam Leventhal <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice! Thanks again for both taking this on and sticking with it through all the design iteration. I don't have anything major here but a few things to fix up, plus: for all breaking changes I generally ask:
- We should update CHANGELOG.adoc. it should have super easy-to-follow instructions there for updating across this change. There should be plenty of examples -- usually we try to say how to know you're affected and exactly what code changes to make, with an example or two.
- I think it's worth doing a test-update of Omicron to a dropshot with this change. We're going to have to do it anyway and doing it first sometimes smokes out cases where the breaking change was worse than we thought to move past or stuff like that.
I did not review the bad-endpoint error output, the OpenAPI spec changes, or the dropshot_endpoint changes FWIW.
Co-authored-by: David Pacheco <[email protected]>
Co-authored-by: David Pacheco <[email protected]>
+ | ||
For details on how to implement `HttpResponseError` for user-defined types, see | ||
the trait documentation, or | ||
https://github.com/oxidecomputer/dropshot/blob/main/dropshot/examples/custom-error.rs[`examples/custom-error.rs`]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this link will be broken until this PR has merged --- it still felt like the nicest way to reference the example in the changelog, IMO.
Okay, @davepacheco, I've updated Omicron to use this branch in oxidecomputer/omicron#7196, and uncovered an issue with the generated OpenAPI docs, which I've resolved (see #1180). I've added changelog entries describing the breaking changes and the migration instructions; let me know what you think of https://github.com/oxidecomputer/dropshot/blob/75cbc09542cf439412c4c13ee2b799f1dff1f28a/CHANGELOG.adoc#unreleased-changes-release-date-tbd |
Currently, all endpoint handler functions must return a
Result<T, HttpError>
, whereT
implementsHttpResponse
. This isunfortunate, as it limits how error return values are represented in
the API. There isn't presently a mechanism for an endpoint to return
a structured error value of its own which is part of the OpenAPI spec
for that endpoint. This is discussed at length in issue #39.
This branch relaxes this requirement, and instead allows endpoint
handler functions to return
Result<T, E>
, whereE
is any type thatimplements a new
HttpResponseError
trait.The
HttpResponseError
trait defines how to produce an error responsefor an error value. This is implemented by
dropshot
'sHttpError
type, but it may also be implemented by user errors. Types implementing
this trait must implement
HttpResponseContent
, to determine how togenerate the response body and define its schema, and they must also
implement a method
HttpResponseError::status_code
to provide thestatus code to use for the error response. This is somewhat different
from the existing
HttpCodedResponse
trait, which allows successfulresponses to indicate at compile time that they will always have a
particular status code, such as 201 Created. Errors are a bit different:
we would like to be able to return any number of different error status
codes, but would still like to ensure that they represent errors, in
order to correctly generate an OpenAPI document where the error schemas
are returned only for error responses (see this comment for
details). As discussed here, we ensure this by providing new
ErrorStatusCode
andClientErrorStatusCode
types, which are newtypesaround
http::StatusCode
that only contain a 4xx or 5xx status (in thecase of
ErrorStatusCode
), or only contain a 4xx (in the case ofClientErrorStatusCode
). These types may be fallibly converted from anhttp::StatusCode
at runtime, but we also provide constants forwell-known 4xx and 5xx statuses, which can be used infallibly. The
HttpResponseError::status_code
method returns anErrorStatusCode
rather than a
http::StatusCode
, allowing us to ensure that error typesalways have error statuses and generate a correct OpenAPI document.
Additionally, while adding
ErrorStatusCode
s, I've gone ahead andchanged the
dropshot::HttpError
type to also use it, and changed theHttpError::for_client_error
andHttpError::for_status
constructorsto take a
ClientErrorStatusCode
. Although this is a breaking change,it resolves a long-standing issue with these APIs: currently, they
assert that the provided status code is a 4xx internally, which is often
surprising to the user. Thus, this PR fixes #693.
Fixes #39
Fixes #693
Fixes #801
This branch is a second attempt at the change originally proposed in PR
#1164, so this closes #1164. This design is substantially simpler, and
only addresses the ability for handler functions to return
user-defined types. Other changes made in #1164, such as a way to
specify a global handler for dropshot-generated errors, and adding
headers to
HttpError
responses, can be addressed separately. For now,all extractors and internal errors still produce
dropshot::HttpError
s.A subsequent change will implement a mechanism for providing alternate
presentation for such errors (such as an HTML 404 page).